import { Slice } from "../model"

import { Step, StepResult } from "./step"
import { StepMap } from "./map"

// ::- Replace a part of the document with a slice of new content.
export class ReplaceStep extends Step {
  // :: (number, number, Slice, ?bool)
  // The given `slice` should fit the 'gap' between `from` and
  // `to`—the depths must line up, and the surrounding nodes must be
  // able to be joined with the open sides of the slice. When
  // `structure` is true, the step will fail if the content between
  // from and to is not just a sequence of closing and then opening
  // tokens (this is to guard against rebased replace steps
  // overwriting something they weren't supposed to).
  constructor(from, to, slice, structure) {
    super()
    this.from = from
    this.to = to
    this.slice = slice
    this.structure = !!structure
  }

  apply(doc) {
    if (this.structure && contentBetween(doc, this.from, this.to))
      return StepResult.fail("Structure replace would overwrite content")
    return StepResult.fromReplace(doc, this.from, this.to, this.slice)
  }

  getMap() {
    return new StepMap([this.from, this.to - this.from, this.slice.size])
  }

  invert(doc) {
    return new ReplaceStep(
      this.from,
      this.from + this.slice.size,
      doc.slice(this.from, this.to)
    )
  }

  map(mapping) {
    let from = mapping.mapResult(this.from, 1),
      to = mapping.mapResult(this.to, -1)
    if (from.deleted && to.deleted) return null
    return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice)
  }

  merge(other) {
    if (!(other instanceof ReplaceStep) || other.structure != this.structure)
      return null

    if (
      this.from + this.slice.size == other.from &&
      !this.slice.openEnd &&
      !other.slice.openStart
    ) {
      let slice =
        this.slice.size + other.slice.size == 0
          ? Slice.empty
          : new Slice(
              this.slice.content.append(other.slice.content),
              this.slice.openStart,
              other.slice.openEnd
            )
      return new ReplaceStep(
        this.from,
        this.to + (other.to - other.from),
        slice,
        this.structure
      )
    } else if (
      other.to == this.from &&
      !this.slice.openStart &&
      !other.slice.openEnd
    ) {
      let slice =
        this.slice.size + other.slice.size == 0
          ? Slice.empty
          : new Slice(
              other.slice.content.append(this.slice.content),
              other.slice.openStart,
              this.slice.openEnd
            )
      return new ReplaceStep(other.from, this.to, slice, this.structure)
    } else {
      return null
    }
  }

  toJSON() {
    let json = { stepType: "replace", from: this.from, to: this.to }
    if (this.slice.size) json.slice = this.slice.toJSON()
    if (this.structure) json.structure = true
    return json
  }

  static fromJSON(schema, json) {
    if (typeof json.from != "number" || typeof json.to != "number")
      throw new RangeError("Invalid input for ReplaceStep.fromJSON")
    return new ReplaceStep(
      json.from,
      json.to,
      Slice.fromJSON(schema, json.slice),
      !!json.structure
    )
  }
}

Step.jsonID("replace", ReplaceStep)

// ::- Replace a part of the document with a slice of content, but
// preserve a range of the replaced content by moving it into the
// slice.
export class ReplaceAroundStep extends Step {
  // :: (number, number, number, number, Slice, number, ?bool)
  // Create a replace-around step with the given range and gap.
  // `insert` should be the point in the slice into which the content
  // of the gap should be moved. `structure` has the same meaning as
  // it has in the [`ReplaceStep`](#transform.ReplaceStep) class.
  constructor(from, to, gapFrom, gapTo, slice, insert, structure) {
    super()
    this.from = from
    this.to = to
    this.gapFrom = gapFrom
    this.gapTo = gapTo
    this.slice = slice
    this.insert = insert
    this.structure = !!structure
  }

  apply(doc) {
    if (
      this.structure &&
      (contentBetween(doc, this.from, this.gapFrom) ||
        contentBetween(doc, this.gapTo, this.to))
    )
      return StepResult.fail("Structure gap-replace would overwrite content")

    let gap = doc.slice(this.gapFrom, this.gapTo)
    if (gap.openStart || gap.openEnd)
      return StepResult.fail("Gap is not a flat range")
    let inserted = this.slice.insertAt(this.insert, gap.content)
    if (!inserted) return StepResult.fail("Content does not fit in gap")
    return StepResult.fromReplace(doc, this.from, this.to, inserted)
  }

  getMap() {
    return new StepMap([
      this.from,
      this.gapFrom - this.from,
      this.insert,
      this.gapTo,
      this.to - this.gapTo,
      this.slice.size - this.insert,
    ])
  }

  invert(doc) {
    let gap = this.gapTo - this.gapFrom
    return new ReplaceAroundStep(
      this.from,
      this.from + this.slice.size + gap,
      this.from + this.insert,
      this.from + this.insert + gap,
      doc
        .slice(this.from, this.to)
        .removeBetween(this.gapFrom - this.from, this.gapTo - this.from),
      this.gapFrom - this.from,
      this.structure
    )
  }

  map(mapping) {
    let from = mapping.mapResult(this.from, 1),
      to = mapping.mapResult(this.to, -1)
    let gapFrom = mapping.map(this.gapFrom, -1),
      gapTo = mapping.map(this.gapTo, 1)
    if ((from.deleted && to.deleted) || gapFrom < from.pos || gapTo > to.pos)
      return null
    return new ReplaceAroundStep(
      from.pos,
      to.pos,
      gapFrom,
      gapTo,
      this.slice,
      this.insert,
      this.structure
    )
  }

  toJSON() {
    let json = {
      stepType: "replaceAround",
      from: this.from,
      to: this.to,
      gapFrom: this.gapFrom,
      gapTo: this.gapTo,
      insert: this.insert,
    }
    if (this.slice.size) json.slice = this.slice.toJSON()
    if (this.structure) json.structure = true
    return json
  }

  static fromJSON(schema, json) {
    if (
      typeof json.from != "number" ||
      typeof json.to != "number" ||
      typeof json.gapFrom != "number" ||
      typeof json.gapTo != "number" ||
      typeof json.insert != "number"
    )
      throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON")
    return new ReplaceAroundStep(
      json.from,
      json.to,
      json.gapFrom,
      json.gapTo,
      Slice.fromJSON(schema, json.slice),
      json.insert,
      !!json.structure
    )
  }
}

Step.jsonID("replaceAround", ReplaceAroundStep)

function contentBetween(doc, from, to) {
  let $from = doc.resolve(from),
    dist = to - from,
    depth = $from.depth
  while (
    dist > 0 &&
    depth > 0 &&
    $from.indexAfter(depth) == $from.node(depth).childCount
  ) {
    depth--
    dist--
  }
  if (dist > 0) {
    let next = $from.node(depth).maybeChild($from.indexAfter(depth))
    while (dist > 0) {
      if (!next || next.isLeaf) return true
      next = next.firstChild
      dist--
    }
  }
  return false
}
